/**
 * 生产环境构建脚本
 *
 * 此脚本用于生成生产环境的构建产物并拼接 TypeScript 类型定义文件。
 *
 * 使用方式：
 * - 指定要构建的包名和输出格式（默认使用包的 buildOptions.formats 或 "esm,cjs"）
 *
 * 例子：
 * ```bash
 * # 包名支持模糊匹配，会构建所有名称包含 "dom" 的包
 * nr build dom
 *
 * # 指定输出格式
 * nr build core --formats cjs
 * ```
 *
 * @ts-check
 */

import fs from 'node:fs'
import { parseArgs } from 'node:util'
import path from 'node:path'
import { brotliCompressSync, gzipSync } from 'node:zlib'
import pico from 'picocolors'
import { cpus } from 'node:os'
import { targets as allTargets, exec, fuzzyMatchTarget } from './utils.js'
import { scanEnums } from './inline-enums.js'
import prettyBytes from 'pretty-bytes'
import { spawnSync } from 'node:child_process'

// 获取当前 Git 提交的短哈希值，用于构建版本标识
const commit = spawnSync('git', ['rev-parse', '--short=7', 'HEAD'])
  .stdout.toString()
  .trim()

/**
 * 解析命令行参数
 * 支持的参数：
 * - formats: 指定输出格式（esm,cjs,global 等）
 * - devOnly: 仅构建开发版本
 * - prodOnly: 仅构建生产版本
 * - withTypes: 同时构建 TypeScript 类型定义文件
 * - sourceMap: 生成 source map 文件
 * - release: 发布模式构建
 * - all: 构建所有匹配的包
 * - size: 生成大小报告
 */
const { values, positionals: targets } = parseArgs({
  allowPositionals: true,
  options: {
    formats: {
      type: 'string',
      short: 'f',
    },
    devOnly: {
      type: 'boolean',
      short: 'd',
    },
    prodOnly: {
      type: 'boolean',
      short: 'p',
    },
    withTypes: {
      type: 'boolean',
      short: 't',
    },
    sourceMap: {
      type: 'boolean',
      short: 's',
    },
    release: {
      type: 'boolean',
    },
    all: {
      type: 'boolean',
      short: 'a',
    },
    size: {
      type: 'boolean',
    },
  },
})

// 将参数解构到变量
const {
  formats, // 指定输出格式
  all: buildAllMatching, // 是否构建所有匹配的包
  devOnly, // 仅构建开发版本
  prodOnly, // 仅构建生产版本
  withTypes: buildTypes, // 是否构建类型定义文件
  sourceMap, // 是否生成 source map
  release: isRelease, // 是否为发布模式
  size: writeSize, // 是否生成大小报告
} = values

// 大小报告输出目录
const sizeDir = path.resolve('temp/size')

// 启动构建流程
run()

/**
 * 主要构建函数
 * 执行完整的构建流程，包括：
 * 1. 初始化构建环境
 * 2. 扫描和缓存枚举
 * 3. 解析构建目标
 * 4. 执行并行构建
 * 5. 检查构建产物大小
 * 6. 生成类型定义文件（可选）
 */
async function run() {
  // 如果需要生成大小报告，先创建目录
  if (writeSize) fs.mkdirSync(sizeDir, { recursive: true })

  // 扫描项目中的枚举并生成缓存
  const removeCache = scanEnums()

  try {
    // 解析要构建的目标包
    const resolvedTargets = targets.length
      ? fuzzyMatchTarget(targets, buildAllMatching) // 模糊匹配指定的目标
      : allTargets // 使用所有可用目标

    // 执行并行构建
    await buildAll(resolvedTargets)

    // 检查构建产物大小
    await checkAllSizes(resolvedTargets)

    // 如果指定了构建类型定义文件，运行 dts 构建
    if (buildTypes) {
      await exec(
        'pnpm',
        [
          'run',
          'build-dts',
          ...(targets.length
            ? ['--environment', `TARGETS:${resolvedTargets.join(',')}`]
            : []),
        ],
        {
          stdio: 'inherit',
        },
      )
    }
  } finally {
    // 无论构建是否成功，都要清理枚举缓存
    removeCache()
  }
}

/**
 * 并行构建所有目标包
 * 利用系统 CPU 核数进行并行构建，提高构建效率
 *
 * @param {Array<string>} targets - 要构建的目标包数组
 * @returns {Promise<void>} - 返回构建过程的 Promise
 */
async function buildAll(targets) {
  await runParallel(cpus().length, targets, build)
}

/**
 * 并行执行函数
 * 在指定的并发数下并行执行迭代器函数
 * 使用 Promise.race 来控制并发数，避免同时运行过多任务
 *
 * @template T - 数据源中元素的类型
 * @param {number} maxConcurrency - 最大并发数
 * @param {Array<T>} source - 数据源数组
 * @param {(item: T) => Promise<void>} iteratorFn - 迭代器函数
 * @returns {Promise<void[]>} - 包含所有迭代结果的 Promise 数组
 */
async function runParallel(maxConcurrency, source, iteratorFn) {
  /**@type {Promise<void>[]} 所有任务的 Promise 数组 */
  const ret = []
  /**@type {Promise<void>[]} 正在执行的任务 Promise 数组 */
  const executing = []

  for (const item of source) {
    // 为每个元素创建一个 Promise
    const p = Promise.resolve().then(() => iteratorFn(item))
    ret.push(p)

    if (maxConcurrency <= source.length) {
      // 创建一个完成后从执行队列中移除的 Promise
      const e = p.then(() => {
        executing.splice(executing.indexOf(e), 1)
      })
      executing.push(e)

      // 如果达到最大并发数，等待一个任务完成
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing)
      }
    }
  }

  // 等待所有任务完成
  return Promise.all(ret)
}

// 读取私有包目录列表
const privatePackages = fs.readdirSync('packages-private')

/**
 * 构建指定的目标包
 * 执行单个包的构建过程，包括环境设置和 Rollup 执行
 *
 * @param {string} target - 要构建的目标包名
 * @returns {Promise<void>} - 返回构建过程的 Promise
 */
async function build(target) {
  // 判断是私有包还是公开包，决定使用哪个基础目录
  const pkgBase = privatePackages.includes(target)
    ? `packages-private` // 私有包目录
    : `packages` // 公开包目录

  const pkgDir = path.resolve(`${pkgBase}/${target}`)
  const pkg = JSON.parse(fs.readFileSync(`${pkgDir}/package.json`, 'utf-8'))

  // 如果是全量构建（没有指定具体目标），则忽略私有包
  if ((isRelease || !targets.length) && pkg.private) {
    return
  }

  // 如果没有指定格式且 dist 目录存在，则清空它
  if (!formats && fs.existsSync(`${pkgDir}/dist`)) {
    fs.rmSync(`${pkgDir}/dist`, { recursive: true })
  }

  // 确定构建环境：优先使用包配置，其次根据 devOnly 参数决定
  const env =
    (pkg.buildOptions && pkg.buildOptions.env) ||
    (devOnly ? 'development' : 'production')

  // 执行 Rollup 构建，传递相关环境变量
  await exec(
    'rollup',
    [
      '-c', // 使用配置文件
      '--environment', // 设置环境变量
      [
        `COMMIT:${commit}`, // Git 提交哈希
        `NODE_ENV:${env}`, // Node.js 环境
        `TARGET:${target}`, // 目标包
        formats ? `FORMATS:${formats}` : ``, // 输出格式
        prodOnly ? `PROD_ONLY:true` : ``, // 仅生产版本
        sourceMap ? `SOURCE_MAP:true` : ``, // 生成 source map
      ]
        .filter(Boolean) // 过滤空值
        .join(','), // 用逗号连接
    ],
    { stdio: 'inherit' }, // 继承标准输入输出
  )
}

/**
 * 检查所有目标包的构建产物大小
 * 仅在非开发模式且包含 global 格式时执行大小检查
 *
 * @param {string[]} targets - 要检查大小的目标包数组
 * @returns {Promise<void>}
 */
async function checkAllSizes(targets) {
  // 如果是开发模式或指定了格式但不包含 global，则跳过大小检查
  if (devOnly || (formats && !formats.includes('global'))) {
    return
  }

  console.log()

  // 逐个检查每个目标包的大小
  for (const target of targets) {
    await checkSize(target)
  }

  console.log()
}

/**
 * 检查单个目标包的构建产物大小
 * 检查 global 和 global-runtime 两种格式的生产版本文件
 *
 * @param {string} target - 要检查大小的目标包
 * @returns {Promise<void>}
 */
async function checkSize(target) {
  const pkgDir = path.resolve(`packages/${target}`)

  // 检查标准 global 生产版本文件
  await checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`)

  // 如果没有指定格式或包含 global-runtime 格式，检查 runtime 版本
  if (!formats || formats.includes('global-runtime')) {
    await checkFileSize(`${pkgDir}/dist/${target}.runtime.global.prod.js`)
  }
}

/**
 * 检查单个文件的大小
 * 计算原始大小、gzip 压缩后大小和 brotli 压缩后大小
 * 并输出到控制台，同时可选地保存报告数据
 *
 * @param {string} filePath - 要检查的文件路径
 * @returns {Promise<void>}
 */
async function checkFileSize(filePath) {
  // 如果文件不存在，直接返回
  if (!fs.existsSync(filePath)) {
    return
  }

  // 读取文件内容
  const file = fs.readFileSync(filePath)
  const fileName = path.basename(filePath)

  // 计算不同压缩算法下的文件大小
  const gzipped = gzipSync(file) // gzip 压缩
  const brotli = brotliCompressSync(file) // brotli 压缩

  // 输出文件大小信息到控制台
  console.log(
    `${pico.gray(pico.bold(fileName))} min:${prettyBytes(
      file.length,
    )} / gzip:${prettyBytes(gzipped.length)} / brotli:${prettyBytes(
      brotli.length,
    )}`,
  )

  // 如果需要生成大小报告，将数据保存为 JSON 文件
  if (writeSize)
    fs.writeFileSync(
      path.resolve(sizeDir, `${fileName}.json`),
      JSON.stringify({
        file: fileName, // 文件名
        size: file.length, // 原始大小
        gzip: gzipped.length, // gzip 压缩后大小
        brotli: brotli.length, // brotli 压缩后大小
      }),
      'utf-8',
    )
}
